#include <memory>
#include <iostream>

#include "client.hpp"
#include "v3/action_constants.hpp"
#include "v3/transaction.hpp"

#include "v3/async/rsp_txn.hpp"
#include "v3/async/rsp_range.hpp"
#include "v3/async/rsp_watch.hpp"
#include "v3/async/rsp_delete.hpp"

#include "v3/async/act_set.hpp"
#include "v3/async/act_compare.hpp"
#include "v3/async/act_update.hpp"
#include "v3/async/act_get.hpp"
#include "v3/async/act_delete.hpp"
#include "v3/async/act_watch.hpp"
#include "v3/async/act_lease.hpp"

using grpc::Channel;

namespace etcd {

Client::Client(std::string const& address) {
    std::string stripped_address;
    std::string substr("://");
    std::string::size_type i = address.find(substr);
    if (i != std::string::npos) {
        stripped_address = address.substr(i + substr.length());
    }
    std::shared_ptr<Channel> channel = grpc::CreateChannel(stripped_address, grpc::InsecureChannelCredentials());
    stub_ = KV::NewStub(channel);
    watchServiceStub = Watch::NewStub(channel);
    leaseServiceStub = Lease::NewStub(channel);
}

pplx::task<Response> Client::get(std::string const& key) {
    etcdv3::ActionParameters params;
    params.key.assign(key);
    params.withPrefix = false;
    params.kv_stub = stub_.get();
    return Response::create(std::make_shared<etcdv3::AsyncGetAction>(params));
}

pplx::task<Response> Client::set(std::string const& key, std::string const& value, int ttl) {
    etcdv3::ActionParameters params;
    params.key.assign(key);
    params.value.assign(value);
    params.kv_stub = stub_.get();

    if (ttl > 0) {
        auto res = leasegrant(ttl).get();
        if (!res.is_ok()) {
            return pplx::task<Response>([res]() {
                return Response(res.error_code(), res.error_message().c_str());
            });
        } else {
            params.lease_id = res.value().lease();
        }
    }
    return Response::create(std::make_shared<etcdv3::AsyncSetAction>(params));
}

pplx::task<Response> Client::set(std::string const& key, std::string const& value, int64_t leaseid) {
    etcdv3::ActionParameters params;
    params.key.assign(key);
    params.value.assign(value);
    params.lease_id = leaseid;
    params.kv_stub = stub_.get();
    return Response::create(std::make_shared<etcdv3::AsyncSetAction>(params));
}

pplx::task<Response> Client::add(std::string const& key, std::string const& value, int ttl) {
    etcdv3::ActionParameters params;
    params.key.assign(key);
    params.value.assign(value);
    params.kv_stub = stub_.get();

    if (ttl > 0) {
        auto res = leasegrant(ttl).get();
        if (!res.is_ok()) {
            return pplx::task<Response>([res]() {
                return Response(res.error_code(), res.error_message().c_str());
            });
        } else {
            params.lease_id = res.value().lease();
        }
    }
    return Response::create(std::make_shared<etcdv3::AsyncSetAction>(params, true));
}

pplx::task<Response> Client::add(std::string const& key, std::string const& value, int64_t leaseid) {
    etcdv3::ActionParameters params;
    params.key.assign(key);
    params.value.assign(value);
    params.lease_id = leaseid;
    params.kv_stub = stub_.get();
    return Response::create(std::make_shared<etcdv3::AsyncSetAction>(params, true));
}

pplx::task<Response> Client::modify(std::string const& key, std::string const& value, int ttl) {
    etcdv3::ActionParameters params;
    params.key.assign(key);
    params.value.assign(value);
    params.kv_stub = stub_.get();

    if (ttl > 0) {
        auto res = leasegrant(ttl).get();
        if (!res.is_ok()) {
            return pplx::task<Response>([res]() {
                return Response(res.error_code(), res.error_message().c_str());
            });
        } else {
            params.lease_id = res.value().lease();
        }
    }
    return Response::create(std::make_shared<etcdv3::AsyncUpdateAction>(params));
}

pplx::task<Response> Client::modify(std::string const& key, std::string const& value, int64_t leaseid) {
    etcdv3::ActionParameters params;
    params.key.assign(key);
    params.value.assign(value);
    params.lease_id = leaseid;
    params.kv_stub = stub_.get();
    return Response::create(std::make_shared<etcdv3::AsyncUpdateAction>(params));
}

pplx::task<Response> Client::modify_if(std::string const& key, std::string const& value, std::string const& old_value, int ttl) {
    etcdv3::ActionParameters params;
    params.key.assign(key);
    params.value.assign(value);
    params.old_value.assign(old_value);
    params.kv_stub = stub_.get();

    if (ttl > 0) {
        auto res = leasegrant(ttl).get();
        if (!res.is_ok()) {
            return pplx::task<Response>([res]() {
                return Response(res.error_code(), res.error_message().c_str());
            });
        } else {
            params.lease_id = res.value().lease();
        }
    }
    return Response::create(std::make_shared<etcdv3::AsyncCompareAndSwapAction>(params, etcdv3::Atomicity_Type::PREV_VALUE));
}

pplx::task<Response> Client::modify_if(std::string const& key, std::string const& value, std::string const& old_value, int64_t leaseid) {
    etcdv3::ActionParameters params;
    params.key.assign(key);
    params.value.assign(value);
    params.old_value.assign(old_value);
    params.lease_id = leaseid;
    params.kv_stub = stub_.get();
    return Response::create(std::make_shared<etcdv3::AsyncCompareAndSwapAction>(params, etcdv3::Atomicity_Type::PREV_VALUE));
}

pplx::task<Response> Client::modify_if(std::string const& key, std::string const& value, int old_index, int ttl) {
    etcdv3::ActionParameters params;
    params.key.assign(key);
    params.value.assign(value);
    params.old_revision = old_index;
    params.kv_stub = stub_.get();
    if (ttl > 0) {
        auto res = leasegrant(ttl).get();
        if (!res.is_ok()) {
            return pplx::task<Response>([res]() {
                return Response(res.error_code(), res.error_message().c_str());
            });
        } else {
            params.lease_id = res.value().lease();
        }
    }
    return Response::create(std::make_shared<etcdv3::AsyncCompareAndSwapAction>(params, etcdv3::Atomicity_Type::PREV_INDEX));
}

pplx::task<Response> Client::modify_if(std::string const& key, std::string const& value, int old_index, int64_t leaseid) {
    etcdv3::ActionParameters params;
    params.key.assign(key);
    params.value.assign(value);
    params.lease_id = leaseid;
    params.old_revision = old_index;
    params.kv_stub = stub_.get();
    return Response::create(std::make_shared<etcdv3::AsyncCompareAndSwapAction>(params, etcdv3::Atomicity_Type::PREV_INDEX));
}


pplx::task<Response> Client::rm(std::string const& key) {
    etcdv3::ActionParameters params;
    params.key.assign(key);
    params.withPrefix = false;
    params.kv_stub = stub_.get();
    return Response::create(std::make_shared<etcdv3::AsyncDeleteAction>(params));
}

pplx::task<Response> Client::rm_if(std::string const& key, std::string const& old_value) {
    etcdv3::ActionParameters params;
    params.key.assign(key);
    params.old_value.assign(old_value);
    params.kv_stub = stub_.get();
    return Response::create(std::make_shared<etcdv3::AsyncCompareAndDeleteAction>(params, etcdv3::Atomicity_Type::PREV_VALUE));
}

pplx::task<Response> Client::rm_if(std::string const& key, int old_index) {
    etcdv3::ActionParameters params;
    params.key.assign(key);
    params.old_revision = old_index;
    params.kv_stub = stub_.get();
    return Response::create(std::make_shared<etcdv3::AsyncCompareAndDeleteAction>(params, etcdv3::Atomicity_Type::PREV_INDEX));
}

pplx::task<Response> Client::rmdir(std::string const& key, bool recursive) {
    etcdv3::ActionParameters params;
    params.key.assign(key);
    params.withPrefix = recursive;
    params.kv_stub = stub_.get();
    return Response::create(std::make_shared<etcdv3::AsyncDeleteAction>(params));
}

pplx::task<Response> Client::ls(std::string const& key) {
    etcdv3::ActionParameters params;
    params.key.assign(key);
    params.withPrefix = true;
    params.kv_stub = stub_.get();
    return Response::create(std::make_shared<etcdv3::AsyncGetAction>(params));
}

pplx::task<Response> Client::watch(std::string const& key, bool recursive) {
    etcdv3::ActionParameters params;
    params.key.assign(key);
    params.withPrefix = recursive;
    params.watch_stub = watchServiceStub.get();
    return Response::create(std::make_shared<etcdv3::AsyncWatchAction>(params));
}

pplx::task<Response> Client::watch(std::string const& key, int fromIndex, bool recursive) {
    etcdv3::ActionParameters params;
    params.key.assign(key);
    params.withPrefix = recursive;
    params.revision = fromIndex;
    params.watch_stub = watchServiceStub.get();
    return Response::create(std::make_shared<etcdv3::AsyncWatchAction>(params));
}

pplx::task<Response> Client::leasegrant(int ttl) {
    etcdv3::ActionParameters params;
    params.ttl = ttl;
    params.lease_stub = leaseServiceStub.get();
    return Response::create(std::make_shared<etcdv3::AsyncLeaseGrantAction>(params));
}

}